# 导入必要的库

# 数据分析三巨头
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

# 聚类工具
from sklearn.cluster import KMeans
from skfuzzy.cluster import cmeans
from sklearn.mixture import GaussianMixture

# 用于计算各项聚类精度指标的工具
from sklearn.metrics import silhouette_samples,\
                            calinski_harabasz_score,\
                            davies_bouldin_score

def cluster_effectiveness_evaluation(
        data_for_cluster, label_list, 
        do_print=False):
    r""" 
    一次性计算四种常见的聚类有效性评价指标。

    针对一组经过聚类的数据，输出聚类有效性评价指标：要求数据已经经过
    一种聚类算法的处理，并且获得了 `labels`。综合四种指标可以很方便
    地评价一次聚类的质量。

    聚类有效性评价指标包括轮廓系数、平均轮廓系数、DBI 指数和 CHI 指
    数。

    轮廓系数：通过比较每个对象与自己的聚类的相似性与与其他聚类中的对
    象的相似性来衡量聚类之间的分离程度，取值范围为 `[-1, 1]`，轮廓
    系数越大聚类效果越好。

    平均轮廓系数：对轮廓系数取得 `.mean()` 的结果，可以对聚类结果
    进行整体的评估。取值范围为 `[-1, 1]`，轮廓系数越大聚类效果越
    好。

    DBI 指数：戴维斯-布偶丁指数（Davies-Bouldin Index），在DB指
    数中，簇与簇之间的相似度被定义为两个簇的簇内直径和与簇间距离的比
    值。DBI的值最小是0，值越小，代表聚类效果越好。

    CHI 指数：方差比准则（Calinski-Harabasz Index），本质是簇间
    距离与簇内距离的比值，且整体计算过程与方差计算方式类似。CHI值越
    高，表示聚类效果越好，因为这意味着数据点在聚类之间的分布比在聚类
    内部的分布更分散。

    输入参数
    ----------
    data_for_cluster : pandas.Dataframe
        用于聚类的原始数据

    label_list : pandas.Series
        一组经过聚类算法产生的 `labels`，形如 1, 1, 2, 0... 的
        整数。各个数字与 `data_for_cluster` 中的各个样本点相对
        应，标注这一样本点属于哪个 `class`。

    do_print : bool, optional
        是否直接 `print` 出各个指标的值，默认为 False。由于数据分
        析通常在 IPython Shell 或者 Jupyter Notebook 中进行，
        这个选项通常是不必要的。

    返回值
    -------
    res_dict : dict
        保存四个指标的字典，包含的键：
          - `silhouette_coefficient`
          - `average_silhouette_coefficient`
          - `cluster_score_DBI`
          - `cluster_score_CHI`
          
    
    示例
    ----------

    >>> res_dict = cluster_effectiveness_evaluation(
        data_for_cluster, label_list)

    >>> cluster_effectiveness_evaluation(
        data_for_cluster, label_list, 
        do_print=True)
    """

    silhouetteCoefArr = silhouette_samples(
        data_for_cluster, label_list)
    cluster_score_CHI = calinski_harabasz_score(
        data_for_cluster, label_list)
    cluster_score_DBI = davies_bouldin_score(
        data_for_cluster, label_list)

    if do_print:
        print("Silhouette Coefficient :\n", 
              silhouetteCoefArr)
        print("Average Silhouette Coefficient :", 
              silhouetteCoefArr.mean())
        print("cluster_score_DBI ：", cluster_score_DBI)
        print("cluster_score_ch：", cluster_score_CHI)

    res_dict = {
        "silhouette_coefficient" : silhouetteCoefArr,
        "average_silhouette_coefficient" : silhouetteCoefArr.mean(),
        "cluster_score_DBI" : cluster_score_DBI,
        "cluster_score_CHI" : cluster_score_CHI
    }
    
    return res_dict

class all_3_clusters:
    r"""
    一次性计算 **KMeans、CMeans 和高斯混合聚类** 三种聚类方法全部
    的聚类有效性指标，输出结果字典、绘制表格或图表，通常用于比较聚类
    模型的有效性以便选择符合要求的模型。
    
    由于 KMeans 和 CMeans 要求对数据进行标准化，`__init__()` 会
    对输入的数据 :math:`D` 进行标准化处理：
    
    .. math::
    
        D_{std} = \dfrac{D - \bar D}{\sigma D}
        
    输入参数
    ----------
    data : pandas.DataFrame
        用于聚类的数据，要求全为数字类型。针对非数字类型的数据请使用
        独热编码或连续编号等方式处理。
    start : int, optional
        起始的 k 值，默认为 2
    end : int, optional
        结束的 k 值，默认为 9    
    
    示例
    ----------

    针对一组数据 `data`，要评估 k = 2 到 k = 9 之间 KMeans、
    CMeans 和高斯混合聚类三个聚类模型的聚类有效性，你需要首先导入数据
        
    >>> cluster_models = all_3_clusters(data)
        
    然后在 k 的范围内进行 `fit()` 
    
    >>> cluster_models.fit(start=2, end=9)
    
    绘制结果的对比图：
    
    >>> cluster_models.plot_result()
    """
    
    def __init__(self, data):
        """
        输入参数
        ----------
        data : pandas.DataFrame
            用于聚类的数据，要求全为数字类型。针对非数字类型的数据请使用
            独热编码或连续编号等方式处理。
        start : int, optional
            起始的 k 值，默认为 2
        end : int, optional
            结束的 k 值，默认为 9
        """
        
        self.cluster_data = data

        # 数据标准化
        self.cluster_data_STD = 1.0 * (
            self.cluster_data - self.cluster_data.mean()
            ) / self.cluster_data.std()  
    
        # 创建过程变量
        res_list_dict = {
            'k_list' : np.array([]),
            'asc_list' : np.array([]),
            'dbi_list' : np.array([]),
            'chi_list' : np.array([])
        }
    
        self.all_3_res_dict = {
            'kmeans' : res_list_dict.copy(),
            'cmeans' : res_list_dict.copy(),
            'gaussian_mixture' : res_list_dict.copy(),
        }
    
    def _append_indexs_to_res_list_dict(
            self,
            local_res_list_dict, 
            k, cluster_index_dict):
        """ 将计算取得的聚类有效性指标追加到 `res_list_dict`
        
        实际上创建了过程变量 `local_res_list_dict` 并返回来实现功能
        而没有直接对 `self.all_3_res_dict` 进行操作。

        Parameters
        ----------
        local_res_list_dict : dict
            方法内部定义的过程变量，参数将返回到 
            `self.all_3_res_dict`
        k : int
            当前计算的聚类系数 k 的值
        cluster_index_dict : dict
            函数 `cluster_effectiveness_evaluation()` 返回的包含
            聚类有效性指标的字典

        Returns
        -------
        local_res_list_dict : dict
            包含聚类有效性指标的字典。被定义为局部变量以避免变量空间冲突
        """
    
        local_res_list_dict['k_list'] = np.append(
            local_res_list_dict['k_list'], k)
        
        local_res_list_dict['asc_list'] = np.append(
            local_res_list_dict['asc_list'], 
            cluster_index_dict[
                'average_silhouette_coefficient'])
        
        local_res_list_dict['dbi_list'] = np.append(
            local_res_list_dict['dbi_list'], 
            cluster_index_dict['cluster_score_DBI'])
        
        local_res_list_dict['chi_list'] = np.append(
            local_res_list_dict['chi_list'], 
            cluster_index_dict['cluster_score_CHI'])
    
        return local_res_list_dict
    
    def fit(self, start=2, end=9):
        self.start = start
        self.end = end
        
        for k in range(self.start, self.end + 1):
    
            # KMeans 聚类 
            kmeans_model = KMeans(
                n_clusters = k, 
                n_init =3, # 版本更新之后这个值不能是 'auto'
                max_iter = 500000,
                )  # 分为 k 类
            kmeans_model.fit(self.cluster_data_STD)
    
            # 获取聚类标签
            labels_kmeans = kmeans_model.labels_
    
            # 聚类优度评价
            kmeans_res_dict = cluster_effectiveness_evaluation(
                self.cluster_data_STD, labels_kmeans)
        
            self._append_indexs_to_res_list_dict(
                self.all_3_res_dict['kmeans'], k, 
                kmeans_res_dict)
        
            # CMeans 聚类
        
            center, u, u0, d, jm, p, fpc = cmeans(
                self.cluster_data_STD.T, m=2, c=k, 
                error=0.005, maxiter=500000)
    
            labels_cmeans = []
            # 获取聚类标签
            for i in u:
                labels_cmeans = np.argmax(u, axis=0)
    
            # 聚类优度评价
            cmeans_res_dict = cluster_effectiveness_evaluation(
                self.cluster_data_STD, labels_cmeans)
        
            self._append_indexs_to_res_list_dict(
                self.all_3_res_dict['cmeans'], k, 
                cmeans_res_dict)
        
            # GaussinGaussianMixture 聚类
        
            # 创建 GaussianMixture 对象
            gmc = GaussianMixture(
                n_components=k, covariance_type='full')
            gmc.fit(self.cluster_data) # 拟合模型
        
            gmc_labels = gmc.predict(self.cluster_data)
    
            gaussian_mixture_res_dict = (
                cluster_effectiveness_evaluation(
                self.cluster_data, gmc_labels))
        
            self._append_indexs_to_res_list_dict(
                self.all_3_res_dict['gaussian_mixture'], k, 
                gaussian_mixture_res_dict)
        
        return self.all_3_res_dict
    
    def result(self):
        return self.all_3_res_dict
    
    def result_table(self):
        reform = {(
            outerKey, 
            innerKey
            ): values for outerKey, innerDict in self.all_3_res_dict.items(
            ) for innerKey, values in innerDict.items()
        }
        return pd.DataFrame(reform)
    
    def plot_result(
            self, axes=None, fig_kws=None):
        if fig_kws is None:
            fig_kws={'figsize': (12, 3)}
        if axes == None:
            fig, axes = plt.subplots(
                1, 3, **fig_kws)
        i = 0

        title_list = [
            'Average Silhouette Coefficient', 
            'BDI', 
            'CHI'
        ]

        for metric in [
            'asc_list', 
            'dbi_list', 
            'chi_list']:
            for method, values in (
                self.all_3_res_dict.items()):
                axes[i].plot(
                    values['k_list'], values[metric], label=method)
            axes[i].set_title(title_list[i])
            axes[i].set_xlabel('Number of Clusters (k)')
            axes[i].set_ylabel(None)
            axes[i].legend()
            axes[i].grid(True)
            i = i + 1